# src/gui.py

import os
import sys
import logging
import threading
import tkinter as tk
from tkinter import ttk, filedialog, messagebox

import toml

from src.cli import request_stop, run as run_cli
from src import config as config_module

logging.basicConfig(level=logging.INFO, format="%(asctime)s[%(levelname)s]%(message)s")
logger = logging.getLogger(__name__)


class TextHandler(logging.Handler):
    """Logging handler that writes to a Tkinter Text widget."""

    def __init__(self, text_widget):
        super().__init__()
        self.text_widget = text_widget

    def emit(self, record):
        msg = self.format(record) + "\n"
        # append in main thread, ensure widget is writable during insert and auto-scroll
        try:
            def _append():
                try:
                    # allow insertion then disable to prevent user edits
                    self.text_widget.configure(state="normal")
                except Exception:
                    pass
                try:
                    self.text_widget.insert(tk.END, msg)
                    self.text_widget.see(tk.END)
                except Exception:
                    pass
                try:
                    self.text_widget.configure(state="disabled")
                except Exception:
                    pass

            self.text_widget.after(0, _append)
        except Exception:
            pass


class TcExpProcGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("TcExpProc - Thermodynamic Calculation Experiment Processor")
        self.root.geometry("800x600")

        # shared state
        # default config filename moved to project-specific name
        self.config_path = tk.StringVar(value="tcexp_cfg.toml")
        self.only_exps = tk.StringVar(value=".")
        self.source = tk.StringVar(value="config")  # config | exps
        self.parallel_count = tk.IntVar(value=1)  # Parallel count
        self.log_level = tk.StringVar(value="info")

        self.config_data = {}
        self.flat_config = {}
        self.config_entries = {}  # key -> Entry widget

        # short explanation text for known keys (displayed above each input)
        self.explanations = {
            "alloy_system.comps_file": "Phase composition data CSV file path",
            "alloy_system.tdb_file": "TDB file path",
            "output.output_root": "Output path",
            "thermo_calc.tcp_path": "tcp.exe path in virtual machine",
            "thermo_calc.work_root": "tcp working directory in virtual machine",
            "vmware.vmrun": "vmrun.exe path (usually in VMware platform directory)",
            "vmware.vmx": "VMX path (virtual machine file)",
            "vmware.vm_user": "Virtual machine username",
            "vmware.vm_passwd": "Virtual machine password",
        }

        self.running = False
        self.run_thread = None

        self.create_widgets()
        # set up logger to GUI after widgets created
        self.setup_logger()

    # ----------------- Configuration tools -----------------
    def load_config_file(self, path=None):
        path = path or self.config_path.get()
        try:
            with open(path, "r", encoding="utf-8") as f:
                self.config_data = toml.load(f)
        except Exception as e:
            logger.error(f"Failed to load configuration file: {e}")
            self.config_data = {}
        self.flatten_config()

    def flatten_config(self):
        self.flat_config = {}

        def flatten(data, prefix=""):
            if isinstance(data, dict):
                for key, value in data.items():
                    new_prefix = f"{prefix}.{key}" if prefix else key
                    if isinstance(value, list) and all(not isinstance(v, (dict, list)) for v in value):
                        self.flat_config[new_prefix] = ",".join(str(v) for v in value)
                    elif isinstance(value, (dict, list)):
                        flatten(value, new_prefix)
                    else:
                        self.flat_config[new_prefix] = value
            elif isinstance(data, list):
                for i, value in enumerate(data):
                    new_prefix = f"{prefix}[{i}]"
                    if isinstance(value, (dict, list)):
                        flatten(value, new_prefix)
                    else:
                        self.flat_config[new_prefix] = value

        flatten(self.config_data)

    def unflatten_config(self):
        result = {}
        for key, value in self.flat_config.items():
            current = result
            parts = key.replace("]", "").split("[")
            parts = [p.split(".") for p in parts]
            parts = [item for sublist in parts for item in sublist if item]

            for part in parts[:-1]:
                if part.isdigit():
                    idx = int(part)
                    if not isinstance(current, list):
                        current = []
                    while len(current) <= idx:
                        current.append({})
                    current = current[idx]
                else:
                    if part not in current:
                        current[part] = {}
                    current = current[part]

            last = parts[-1]
            if last.isdigit():
                idx = int(last)
                if not isinstance(current, list):
                    current = []
                while len(current) <= idx:
                    current.append(None)
                current[idx] = self.try_convert_value(value)
            else:
                current[last] = self.try_convert_value(value)

        return result

    def try_convert_value(self, value_str):
        if not isinstance(value_str, str):
            return value_str
        if value_str.lower() == "true":
            return True
        if value_str.lower() == "false":
            return False
        try:
            if "." in value_str:
                return float(value_str)
            return int(value_str)
        except Exception:
            return value_str

    # ----------------- GUI -----------------
    def create_widgets(self):
        nb = ttk.Notebook(self.root)
        nb.pack(fill=tk.BOTH, expand=True)

        # tab 1: runner / logs
        tab1 = ttk.Frame(nb)
        nb.add(tab1, text="Run")

        controls_frame = ttk.Frame(tab1, padding=8)
        controls_frame.pack(fill=tk.X)

        # source selection
        ttk.Label(controls_frame, text="Working mode").grid(row=0, column=0, sticky=tk.W)
        ttk.Radiobutton(controls_frame, text="Batch tcp calculation + post-processing",
                        variable=self.source, value="config",
                        command=self.on_source_change
                        ).grid(row=0, column=1, sticky=tk.W)
        ttk.Radiobutton(controls_frame, text="Post-process exps only",
                        variable=self.source, value="exps",
                        command=self.on_source_change
                        ).grid(row=0, column=2, sticky=tk.W)

        # config path / exps dir selection
        ttk.Label(controls_frame, text="Input path (cfg/exps):").grid(row=1, column=0, sticky=tk.W)
        self.path_entry = ttk.Entry(controls_frame, width=50)
        self.path_entry.grid(row=1, column=1, columnspan=2, sticky="we")
        ttk.Button(controls_frame, text="Browse", command=self.browse_path).grid(row=1, column=3, sticky=tk.W)

        # (file selectors moved to Config Editor tab)

        # parallel count and log level
        parallel_frame = ttk.Frame(controls_frame)
        parallel_frame.grid(row=2, column=0, columnspan=4, sticky="we", pady=(6, 0))
        ttk.Label(parallel_frame, text="Parallel count:").pack(side=tk.LEFT, padx=(0, 8))
        self.parallel_scale = ttk.Scale(parallel_frame, from_=1, to=4, orient=tk.HORIZONTAL,
                                        variable=self.parallel_count, command=self.on_parallel_change)
        self.parallel_scale.configure(length=100)  # Shorten slider length
        self.parallel_scale.pack(side=tk.LEFT, fill=tk.X, expand=False)
        parallel_value = ttk.Label(parallel_frame, textvariable=self.parallel_count)
        parallel_value.pack(side=tk.LEFT, padx=8)

        ttk.Label(controls_frame, text="Log level:").grid(row=3, column=0, sticky=tk.W, pady=(6, 0))
        ttk.Radiobutton(controls_frame, text="info", variable=self.log_level,
                        value="info").grid(row=3, column=1, sticky=tk.W)
        ttk.Radiobutton(controls_frame, text="debug", variable=self.log_level,
                        value="debug").grid(row=3, column=2, sticky=tk.W)

        # run/stop buttons
        btn_frame = ttk.Frame(controls_frame)
        btn_frame.grid(row=4, column=0, columnspan=4, sticky="we", pady=(8, 0))
        self.run_btn = ttk.Button(btn_frame, text="Start", command=self.start_run)
        self.run_btn.pack(side=tk.LEFT, padx=4)
        self.stop_btn = ttk.Button(btn_frame, text="Stop", command=self.stop_run, state=tk.DISABLED)
        self.stop_btn.pack(side=tk.LEFT, padx=4)

        # logs
        log_frame = ttk.Frame(tab1, padding=8)
        log_frame.pack(fill=tk.BOTH, expand=True)
        ttk.Label(log_frame, text="Logs:").pack(anchor=tk.W)
        # Place scrollbar on the right, text area on the left, so scrollbar aligns with text
        log_scroll = ttk.Scrollbar(log_frame, orient=tk.VERTICAL)
        log_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        # Make the text widget read-only by default; TextHandler will temporarily enable it to append
        # Reduce vertical size so footer fits comfortably
        self.log_text = tk.Text(log_frame, height=12, yscrollcommand=log_scroll.set, state="disabled")
        self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        log_scroll.config(command=self.log_text.yview)

        # footer: version and contact (read-only)
        footer_frame = ttk.Frame(tab1)
        footer_frame.pack(fill=tk.X, padx=8, pady=(4, 8))

        # Footer: show Version and Contact in a read-only Text (single-line) so it looks cleaner
        footer_text = tk.Text(footer_frame, height=1, wrap="none", bd=0, highlightthickness=0)
        version = getattr(config_module, "VERSION", "")
        contact = getattr(config_module, "CONTACT", "")
        footer_text.insert("1.0", f"{version}    {contact}")
        footer_text.configure(state="disabled", bg="systembuttonface")
        footer_text.pack(fill=tk.X)

        # tab 2: config editor
        tab2 = ttk.Frame(nb)
        nb.add(tab2, text="Config Editor")

        editor_top = ttk.Frame(tab2, padding=8)
        editor_top.pack(fill=tk.X)
        ttk.Button(editor_top, text="Load Config", command=self.load_config_tab).pack(side=tk.LEFT)
        ttk.Button(editor_top, text="Save Config", command=self.save_config_tab).pack(side=tk.LEFT, padx=(8, 0))
        ttk.Button(editor_top, text="Generate Template", command=self.generate_template).pack(side=tk.LEFT, padx=(8, 0))

    # file selectors will be created dynamically when loading a config

        # scrollable frame for key/value pairs
        self.editor_container = ttk.Frame(tab2)
        self.editor_container.pack(fill=tk.BOTH, expand=True)

        self.editor_canvas = tk.Canvas(self.editor_container)
        self.editor_scroll = ttk.Scrollbar(self.editor_container, orient=tk.VERTICAL,
                                           command=self.editor_canvas.yview)
        self.editor_frame = ttk.Frame(self.editor_canvas)

        self.editor_frame.bind(
            "<Configure>", lambda e: self.editor_canvas.configure(scrollregion=self.editor_canvas.bbox("all"))
        )
        self.editor_canvas.create_window((0, 0), window=self.editor_frame, anchor="nw")
        self.editor_canvas.configure(yscrollcommand=self.editor_scroll.set)

        self.editor_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.editor_scroll.pack(side=tk.RIGHT, fill=tk.Y)

        # ensure the editor_frame columns expand appropriately so values are visible
        try:
            self.editor_frame.grid_columnconfigure(0, weight=0)
            self.editor_frame.grid_columnconfigure(1, weight=1)
            self.editor_frame.grid_columnconfigure(2, weight=0)
        except Exception:
            pass

        # initialize path entry based on source
        self.on_source_change()

    def setup_logger(self):
        handler = TextHandler(self.log_text)
        formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
        handler.setFormatter(formatter)
        # attach to root logger so logs from other modules go to this handler
        root = logging.getLogger()
        root.addHandler(handler)
        # keep a reference to the handler so we can adjust level if needed
        self._gui_log_handler = handler

    # ----------------- helpers -----------------
    def on_parallel_change(self, value):
        # Ensure parallel count is integer
        self.parallel_count.set(int(float(value)))

    def browse_file(self, entry_widget):
        fn = filedialog.askopenfilename()
        if fn:
            entry_widget.delete(0, tk.END)
            entry_widget.insert(0, fn)

    def browse_dir(self, entry_widget):
        dn = filedialog.askdirectory()
        if dn:
            entry_widget.delete(0, tk.END)
            entry_widget.insert(0, dn)

    def generate_template(self):
        # Pop up dialog for user to select save location and write template
        data = config_module.get_default_template()
        path = filedialog.asksaveasfilename(defaultextension=".toml",
                                            filetypes=[("TOML Files", "*.toml")],
                                            title="Save template as")
        if not path:
            return
        try:
            config_module.save_template(path, data)
            messagebox.showinfo("Success", f"Template saved to {path}")
        except Exception as e:
            logger.exception(f"Failed to save template: {e}")
            messagebox.showerror("Error", f"Failed to save template: {e}")

    def generate_template_default(self):
        # Save default template into the directory of the running script as tcexp_cfg.toml
        data = config_module.get_default_template()
        base_dir = os.path.dirname(os.path.abspath(sys.argv[0])) or os.getcwd()
        path = os.path.join(base_dir, "tcexp_cfg.toml")
        try:
            config_module.save_template(path, data)
            messagebox.showinfo("Success", f"Default template saved to {path}")
            return path
        except Exception as e:
            logger.exception(f"Failed to save default template: {e}")
            messagebox.showerror("Error", f"Failed to save default template: {e}")
            return None

    # ----------------- UI callbacks -----------------
    def on_source_change(self):
        if self.source.get() == "config":
            self.path_entry.delete(0, tk.END)
            self.path_entry.insert(0, self.config_path.get())
        else:
            self.path_entry.delete(0, tk.END)
            self.path_entry.insert(0, self.only_exps.get())

    def browse_path(self):
        if self.source.get() == "config":
            fn = filedialog.askopenfilename(title="Select config TOML", filetypes=[
                                            ("TOML files", "*.toml"), ("All files", "*.*")])
            if fn:
                self.config_path.set(fn)
                self.path_entry.delete(0, tk.END)
                self.path_entry.insert(0, fn)
        else:
            dn = filedialog.askdirectory(title="Select EXPs directory")
            if dn:
                self.only_exps.set(dn)
                self.path_entry.delete(0, tk.END)
                self.path_entry.insert(0, dn)

    # ----------------- run/stop -----------------
    def start_run(self):
        if self.running:
            return
        # set root logging level so messages from all modules appear in GUI
        root = logging.getLogger()
        root.setLevel(logging.DEBUG if self.log_level.get() == "debug" else logging.INFO)
        if hasattr(self, "_gui_log_handler"):
            self._gui_log_handler.setLevel(root.level)

        # save current path values
        if self.source.get() == "config":
            cfg = self.path_entry.get()
            self.config_path.set(cfg)
            only_exps = ""
        else:
            only_exps = self.path_entry.get()
            self.only_exps.set(only_exps)
            cfg = ""

        self.run_btn.configure(state=tk.DISABLED)
        self.stop_btn.configure(state=tk.NORMAL)
        self.running = True

        def worker():
            try:
                run_cli(config_path=cfg or self.config_path.get(),
                        parallel=self.parallel_count.get(),
                        only_exps=only_exps)
                logger.info("Run completed")
            except Exception as e:
                logger.exception(f"Run failed: {e}")
            finally:
                self.root.after(0, lambda: self.run_btn.configure(state=tk.NORMAL))
                self.root.after(0, lambda: self.stop_btn.configure(state=tk.DISABLED))
                self.running = False

        self.run_thread = threading.Thread(target=worker, daemon=True)
        self.run_thread.start()

    def stop_run(self):
        if not self.running:
            return
        try:
            request_stop()
            logger.info("Stop request sent")
        except Exception as e:
            logger.exception(f"Failed to send stop request: {e}")

    # ----------------- config editor tab -----------------
    def clear_editor(self):
        for child in self.editor_frame.winfo_children():
            child.destroy()
        self.config_entries = {}

    def load_config_tab(self):
        # load file
        path = self.config_path.get()
        if not os.path.exists(path):
            # Configuration does not exist, try to generate default template and continue
            created = self.generate_template_default()
            if not created:
                messagebox.showwarning("Warning", f"Configuration file not found and default generation failed: {path}")
                return
            # Use the generated default file path
            self.config_path.set(created)
            path = created
        self.load_config_file(path)
        # populate editor
        self.clear_editor()
        row = 0
        # mapping of keys that should have a browse button and which helper to call
        file_selectors = {
            "alloy_system.comps_file": self.browse_file,
            "alloy_system.tdb_file": self.browse_file,
            "output.output_root": self.browse_dir,
            "vmware.vmrun": self.browse_file,
            "vmware.vmx": self.browse_file,
        }

        for key, value in sorted(self.flat_config.items()):
            # explanation label (above the input)
            expl = self.explanations.get(key, "")
            if expl:
                ttk.Label(self.editor_frame, text=expl, foreground="gray").grid(
                    row=row, column=0, columnspan=3, sticky=tk.W, padx=4, pady=(6, 0))
                row += 1

            # key label + value entry
            ttk.Label(self.editor_frame, text=key).grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
            val_str = ",".join(str(v) for v in value) if isinstance(value, list) else str(value)
            e = ttk.Entry(self.editor_frame, width=60)
            e.insert(0, val_str)
            e.grid(row=row, column=1, sticky="we", padx=4, pady=2)
            self.config_entries[key] = e

            # if this key is a file/directory selector, add a Browse button next to the entry
            if key in file_selectors:
                browse_func = file_selectors[key]
                # use default arg binding to capture current entry and func
                btn = ttk.Button(self.editor_frame, text="Browse", command=(lambda ent=e, fn=browse_func: fn(ent)))
                btn.grid(row=row, column=2, sticky=tk.W, padx=4)
            row += 1
        # force canvas redraw and update scrollregion so all rows become visible
        try:
            self.editor_canvas.update_idletasks()
            self.editor_canvas.configure(scrollregion=self.editor_canvas.bbox("all"))
        except Exception:
            pass

        # After loading, synchronize the Run page Path to current config file path for intuitive display
        try:
            self.source.set("config")
            self.path_entry.delete(0, tk.END)
            self.path_entry.insert(0, self.config_path.get())
        except Exception:
            pass

    def save_config_tab(self):
        # collect entries
        for key, e in self.config_entries.items():

            v = e.get()
            self.flat_config[key] = v

        # file selector entries are part of self.config_entries and were saved above
        # rebuild nested structure
        self.config_data = self.unflatten_config()
        # write out
        try:
            with open(self.config_path.get(), "w", encoding="utf-8") as f:
                toml.dump(self.config_data, f)
            messagebox.showinfo("Success", "Configuration saved")
        except Exception as e:
            logger.exception(f"Failed to save configuration: {e}")
            messagebox.showerror("Error", f"Failed to save configuration: {e}")


def gui():
    root = tk.Tk()
    app = TcExpProcGUI(root)
    root.mainloop()


if __name__ == "__main__":
    gui()